/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.meta.service.impl.data.validation;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Objects.isNull;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.unidata.mdm.meta.util.ModelUtils.findModelAttribute;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.Nonnull;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.joda.time.Interval;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.context.ModelSourceContext;
import org.unidata.mdm.core.service.CustomPropertiesSupport;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.data.BinaryLargeValue;
import org.unidata.mdm.core.type.data.CharacterLargeValue;
import org.unidata.mdm.core.type.data.MeasuredValue;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.EntityElement;
import org.unidata.mdm.core.type.model.RelationElement;
import org.unidata.mdm.core.type.model.support.AttributeValueGenerator;
import org.unidata.mdm.core.type.model.support.ExternalIdValueGenerator;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.configuration.TypeIds;
import org.unidata.mdm.meta.service.impl.ModelElementValidator;
import org.unidata.mdm.meta.type.input.meta.MetaType;
import org.unidata.mdm.meta.type.instance.DataModelInstance;
import org.unidata.mdm.meta.type.instance.MeasurementUnitsInstance;
import org.unidata.mdm.meta.type.model.DataModel;
import org.unidata.mdm.meta.type.model.MetaModelAttribute;
import org.unidata.mdm.meta.type.model.PeriodBoundary;
import org.unidata.mdm.meta.type.model.SimpleDataType;
import org.unidata.mdm.meta.type.model.ValueGenerationStrategy;
import org.unidata.mdm.meta.type.model.ValueGenerationStrategyType;
import org.unidata.mdm.meta.type.model.attributes.ArrayMetaModelAttribute;
import org.unidata.mdm.meta.type.model.attributes.AttributeGroup;
import org.unidata.mdm.meta.type.model.attributes.AttributeMeasurementSettings;
import org.unidata.mdm.meta.type.model.attributes.CodeMetaModelAttribute;
import org.unidata.mdm.meta.type.model.attributes.ComplexMetaModelAttribute;
import org.unidata.mdm.meta.type.model.attributes.SimpleMetaModelAttribute;
import org.unidata.mdm.meta.type.model.attributes.SimpleTypeMetaModelAttribute;
import org.unidata.mdm.meta.type.model.entities.AbstractEntity;
import org.unidata.mdm.meta.type.model.entities.ComplexAttributesHolderEntity;
import org.unidata.mdm.meta.type.model.entities.EntitiesGroup;
import org.unidata.mdm.meta.type.model.entities.Entity;
import org.unidata.mdm.meta.type.model.entities.LookupEntity;
import org.unidata.mdm.meta.type.model.entities.NestedEntity;
import org.unidata.mdm.meta.type.model.entities.RelType;
import org.unidata.mdm.meta.type.model.entities.Relation;
import org.unidata.mdm.meta.type.model.enumeration.EnumerationType;
import org.unidata.mdm.meta.type.model.enumeration.EnumerationsModel;
import org.unidata.mdm.meta.type.model.measurement.MeasurementCategory;
import org.unidata.mdm.meta.type.model.measurement.MeasurementUnitsModel;
import org.unidata.mdm.meta.type.model.merge.MergeAttribute;
import org.unidata.mdm.meta.type.model.sourcesystem.SourceSystem;
import org.unidata.mdm.meta.type.model.strategy.CustomValueGenerationStrategy;
import org.unidata.mdm.meta.util.ModelUtils;
import org.unidata.mdm.meta.util.ValidityPeriodUtils;
import org.unidata.mdm.system.exception.ValidationResult;
import org.unidata.mdm.system.util.ConvertUtils;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;

/**
 * The Class MetaModelValidationComponentImpl.
 */
@Component
public class DataModelValidationComponent implements CustomPropertiesSupport {

    /**
     * ALLOW: Message codes.
     */
    private static final String ALLOW_MEASUREMENT_CATEGORY_IN_USE = "meta.data.allow.measurement.category.in.use";
    private static final String ALLOW_MEASUREMENT_UNIT_IN_USE = "meta.data.allow.measurement.unit.in.use";
    private static final String ALLOW_ENUMERATION_IN_USE = "meta.data.allow.enumeration.in.use";

    /**
     * PRECHECK: Message codes.
     */
    private static final String PRECHECK_NESTED_ENTITY_RECURSION_DETECTED = "app.meta.nested.entity.recursion.detected";

    /**
     * VALIDATE: Message's codes
     */
    private static final String MEASUREMENT_UNIT_INCORRECT = "app.meta.measurement.settings.refers.to.undefined.unit";
    private static final String MEASUREMENT_CATEGORY_INCORRECT = "app.meta.measurement.settings.refers.to.undefined.category";
    private static final String UNKNOWN_ATTR_IN_DISPLAY_GROUP = "app.meta.display.group.contain.absent.attr";
    private static final String MERGE_SOURCE_SYSTEM_ABSENT = "app.meta.merge.source.system.absent";
    private static final String MERGE_SOURCE_SYSTEM_INCORRECT = "app.meta.merge.source.system.incorrect";
    private static final String MERGE_ATTR_ABSENT = "app.meta.merge.attr.absent";
    private static final String MERGE_ATTR_INCORRECT = "app.meta.merge.attr.incorrect";
    private static final String ENUMERATION_ABSENT = "app.meta.enum.absent";
    private static final String LOOKUP_ENTITY_ABSENT = "app.meta.lookup.absent";
    private static final String NESTED_ENTITY_NAME_ABSENT = "app.meta.nested.entity.name.absent";
    private static final String NESTED_ENTITY_ABSENT = "app.meta.nested.entity.absent";
    private static final String RELATION_FROM_ENTITY_ABSENT = "app.meta.relation.from.entity.absent";
    private static final String RELATION_TO_ENTITY_ABSENT = "app.meta.relation.to.entity.absent";
    private static final String NODE_GROUP_TREE_ABSENT = "app.meta.group.node.absent";
    private static final String DUPL_ATTRIBUTE = "app.meta.attr.dupl";
    private static final String NO_SEARCHABLE_ATTRIBUTE = "app.meta.attr.noSearchable";
    private static final String LINK_ATTRIBUTE_INCORRECT = "app.meta.attr.link.incorrect";
    private static final String TIMELINE_NOT_OVERLAPPING = "app.meta.timeline.overlapping";
    private static final String MODIFIED_RELATION_TYPE_ON_ELEMENT_WITH_DATA = "app.meta.relation.type.modified";
    private static final String ATTRIBUTE_GENERATION_STRATEGY_INCORRECT = "app.meta.attribute.generation.strategy.incorrect";
    private static final String ENTITY_GENERATION_STRATEGY_INCORRECT = "app.meta.entity.generation.strategy.incorrect";
    private static final String ROOT_GROUP_IS_ABSENT = "app.meta.root.group.is.absent";
    private static final String DUPLICATE_ENTITIY_NAMES = "app.meta.duplicate.entity.names";

    private static final Date MIN_DATE_VALUE = new Date(Long.MIN_VALUE);

    private static final Date MAX_DATE_VALUE = new Date(Long.MAX_VALUE);

    private static final Interval NULL_INTERVAL_VALUE = new Interval(Long.MIN_VALUE, Long.MAX_VALUE);
    /**
     * The meta model service.
     */
    @Autowired
    private MetaModelService metaModelService;
    /**
     * Base validators.
     */
    @Autowired
    private List<ModelElementValidator<?>> validators;

    public Collection<ValidationResult> allow(ModelSourceContext<?> change) {

        Collection<ValidationResult> validations = new HashSet<>();

        validations.addAll(allowMeasurementUnitsUpdate(change));
        validations.addAll(allowEnumerationsUpdate(change));

        return validations;
    }

    /* (non-Javadoc)
     * @see com.unidata.mdm.backend.service.model.draft.MetaDraftValidationComponent#checkReferencesToMeasurementValues(java.util.List)
     */
    private Collection<ValidationResult> allowMeasurementUnitsUpdate(@Nonnull final ModelSourceContext<?> change) {

        if (!StringUtils.equals(change.getTypeId(), TypeIds.MEASUREMENT_UNITS_MODEL)) {
            return Collections.emptyList();
        }

        MeasurementUnitsModel source = (MeasurementUnitsModel) change.getSource();
        Collection<ValidationResult> validations = new HashSet<>();

        DataModelInstance i = metaModelService.instance(Descriptors.DATA);
        List<EntityElement> elements = new ArrayList<>();

        elements.addAll(i.getLookups());
        elements.addAll(i.getRegisters());
        elements.addAll(i.getRelations());
        elements.addAll(i.getNested());

        elements.forEach(el ->

            el.getAttributes().values().stream()
                .filter(AttributeElement::isMeasured)
                .forEach(attr -> {

                    MeasurementCategory category = source.getValues().stream()
                            .filter(c -> StringUtils.equals(attr.getMeasured().getCategoryId(), c.getName()))
                            .findFirst()
                            .orElse(null);

                    if (Objects.isNull(category)) {
                        validations.add(new ValidationResult("Measurement category [{}] is in use by attribute [{}] in data model element [{}].",
                                ALLOW_MEASUREMENT_CATEGORY_IN_USE, attr.getMeasured().getCategoryId(), attr.getName(), el.getDisplayName()));
                    } else if (category.getUnits().stream().noneMatch(u -> StringUtils.equals(attr.getMeasured().getDefaultUnitId(), u.getName()))) {
                        validations.add(new ValidationResult("Measurement unit [{}] is in use by attribute [{}] in data model element [{}].",
                                ALLOW_MEASUREMENT_UNIT_IN_USE, attr.getMeasured().getDefaultUnitId(), attr.getName(), el.getDisplayName()));
                    }

                })
        );

        return validations;
    }

    /* (non-Javadoc)
     * @see com.unidata.mdm.backend.service.model.draft.MetaDraftValidationComponent#checkReferencesToMeasurementValues(java.util.List)
     */
    private Collection<ValidationResult> allowEnumerationsUpdate(@Nonnull final ModelSourceContext<?> change) {

        if (!StringUtils.equals(change.getTypeId(), TypeIds.ENUMERATIONS_MODEL)) {
            return Collections.emptyList();
        }

        EnumerationsModel source = (EnumerationsModel) change.getSource();
        Collection<ValidationResult> validations = new HashSet<>();

        DataModelInstance i = metaModelService.instance(Descriptors.DATA);
        List<EntityElement> elements = new ArrayList<>();

        elements.addAll(i.getLookups());
        elements.addAll(i.getRegisters());
        elements.addAll(i.getRelations());
        elements.addAll(i.getNested());

        elements.forEach(el ->

            el.getAttributes().values().stream()
                .filter(AttributeElement::isEnumValue)
                .forEach(attr -> {

                    EnumerationType enumeration = source.getValues().stream()
                            .filter(en -> StringUtils.equals(attr.getEnumName(), en.getName()))
                            .findFirst()
                            .orElse(null);

                    if (Objects.isNull(enumeration)) {
                        validations.add(new ValidationResult("Enumeration type [{}] is in use by attribute [{}] in data model element [{}].",
                                ALLOW_ENUMERATION_IN_USE, attr.getEnumName(), attr.getName(), el.getDisplayName()));
                    }
                })
        );

        return validations;
    }

    /*
     * FIXME: Implement or remove.
     * (non-Javadoc)
     * @see com.unidata.mdm.backend.service.model.draft.MetaDraftValidationComponent#checkReferencesToMeasurementValues(java.util.List)
     */
//    private Collection<ValidationResult> allowSourceSystemsUpdate(@Nonnull final ModelSourceContext<?> change) {
//
//        if (!StringUtils.equals(change.getTypeId(), TypeIds.SOURCE_SYSTEMS_MODEL)) {
//            return Collections.emptyList();
//        }
//
//        SourceSystemsModel source = (SourceSystemsModel) change.getSource();
//        Collection<ValidationResult> validations = new HashSet<>();
//
//        DataModelInstance i = metaModelService.instance(Descriptors.DATA);
//        List<EntityElement> elements = new ArrayList<>();
//
//        elements.addAll(i.getLookups());
//        elements.addAll(i.getRegisters());
//
//        Map<String, Set<String>> hits = new HashMap<>();
//
//        elements.forEach(el -> {
//
//        });
//
//        return validations;
//    }

    public Collection<ValidationResult> precheck(DataModel model) {

        Collection<ValidationResult> validations = new HashSet<>();
        model.getNestedEntities().forEach(ne -> precheckNestedRecursion(ne, Collections.emptySet(), validations, model));

        return validations;
    }

    private void precheckNestedRecursion(NestedEntity current, Set<String> names, Collection<ValidationResult> validations, DataModel model) {

        if (Objects.isNull(current) || CollectionUtils.isEmpty(current.getComplexAttribute())) {
            return;
        }

        Set<String> localNames = new HashSet<>(names);
        localNames.add(current.getName());
        for (ComplexMetaModelAttribute cmma : current.getComplexAttribute()) {

            if (localNames.contains(cmma.getNestedEntityName())) {
                validations.add(new ValidationResult("Recursive links to Nested Entity [{}] detected. This is not allowed.",
                        PRECHECK_NESTED_ENTITY_RECURSION_DETECTED, cmma.getNestedEntityName()));
            } else {

                NestedEntity hit = model.getNestedEntities().stream()
                    .filter(ne -> cmma.getNestedEntityName().equals(ne.getName()))
                    .findFirst()
                    .orElse(null);

                precheckNestedRecursion(hit, localNames, validations, model);
            }
        }
    }

    public Collection<ValidationResult> validate(DataModel model) {

        Collection<ValidationResult> validations = new HashSet<>();

        // 0. Check basic stuff
        validations.addAll(validateBase(model));

        // 1. Check groups.
        validations.addAll(validateGroups(model));

        // 2. Check duplicate names
        validations.addAll(validateNames(model));

        // 3. Check attribute groups
        validations.addAll(validateDisplayGroups(model));

        // 4. Check attributes
        validations.addAll(validateAttributes(model));

        // 5. Check merge settings
        validations.addAll(validateMergeSettings(model));

        // 6. Check references to enumerations
        validations.addAll(validateReferencesToEnumerations(model));

        // 7 Check references to lookups
        validations.addAll(validateReferencesToLookups(model));

        // 8. CHeck refrences to nested
        validations.addAll(validateReferencesToNesteds(model));

        // 9. Check references to groups
        validations.addAll(validateReferencesToGroups(model));

        // 10. Check relations
        validations.addAll(validateRelations(model));

        // 11. Check timelines
        validations.addAll(validateTimelines(model));

        // 12. Check MU
        validations.addAll(validateReferencesToMeasurementUnits(model));

        // 13. Check modification of relation type on elements with data
        validations.addAll(validateRelationTypeChange(model));

        return validations;
    }

    /**
     * Check model with basic validators.
     *
     * @param model the model to check
     * @return result
     */
    private Collection<ValidationResult> validateBase(@Nonnull final DataModel model) {

        Collection<ValidationResult> errors = new ArrayList<>();

        List<ModelElementValidator<Entity>> evls = validators.stream()
            .filter(v -> v.getSupportedElementType() == DataModelElementType.REGISTER)
            .map(ModelElementValidator::<Entity>narrow)
            .collect(Collectors.toList());

        if (CollectionUtils.isNotEmpty(evls)) {
            model.getEntities().forEach(el -> evls.forEach(v -> errors.addAll(v.checkElement(el))));
        }

        List<ModelElementValidator<LookupEntity>> lvls = validators.stream()
            .filter(v -> v.getSupportedElementType() == DataModelElementType.LOOKUP)
            .map(ModelElementValidator::<LookupEntity>narrow)
            .collect(Collectors.toList());

        if (CollectionUtils.isNotEmpty(lvls)) {
            model.getLookupEntities().forEach(el -> lvls.forEach(v -> errors.addAll(v.checkElement(el))));
        }

        List<ModelElementValidator<NestedEntity>> nvls = validators.stream()
            .filter(v -> v.getSupportedElementType() == DataModelElementType.NESTED)
            .map(ModelElementValidator::<NestedEntity>narrow)
            .collect(Collectors.toList());

        if (CollectionUtils.isNotEmpty(nvls)) {
            model.getNestedEntities().forEach(el -> nvls.forEach(v -> errors.addAll(v.checkElement(el))));
        }

        List<ModelElementValidator<Relation>> rvls = validators.stream()
            .filter(v -> v.getSupportedElementType() == DataModelElementType.RELATION)
            .map(ModelElementValidator::<Relation>narrow)
            .collect(Collectors.toList());

        if (CollectionUtils.isNotEmpty(rvls)) {
            model.getRelations().forEach(el -> rvls.forEach(v -> errors.addAll(v.checkElement(el))));
        }

        List<ModelElementValidator<EntitiesGroup>> gvls = validators.stream()
            .filter(v -> v.getSupportedElementType() == DataModelElementType.ENTITIES_GROUP)
            .map(ModelElementValidator::<EntitiesGroup>narrow)
            .collect(Collectors.toList());

        if (CollectionUtils.isNotEmpty(gvls) && Objects.nonNull(model.getEntitiesGroup())) {
            gvls.forEach(v -> errors.addAll(v.checkElement(model.getEntitiesGroup())));
        }

        return errors;
    }
    /**
     * Check groups.
     *
     * @param ctx the ctx
     * @return the collection of validation errors
     */
    private Collection<ValidationResult> validateGroups(@Nonnull final DataModel model) {

        Collection<ValidationResult> errors = new ArrayList<>();

        // validate full model.
        EntitiesGroup root = model.getEntitiesGroup();
        if (Objects.isNull(root)) {
            errors.add(new ValidationResult("Root GROUP is absent.", ROOT_GROUP_IS_ABSENT));
        } else {

            Multimap<String, String> groupNames = HashMultimap.create();
            model.getEntities().forEach(ent -> groupNames.put(ent.getGroupName(), ent.getDisplayName()));
            model.getLookupEntities().forEach(ent -> groupNames.put(ent.getGroupName(), ent.getDisplayName()));

            Map<String, String[]> splitGroupNames = groupNames.keySet()
                    .stream()
                    .collect(Collectors.toMap(group -> group, ModelUtils::splitPath));

            for (Map.Entry<String, String[]> splitName : splitGroupNames.entrySet()) {

                String fullGroupName = splitName.getKey();
                Collection<EntitiesGroup> entitiesGroups = singletonList(root);
                for (String namePath : splitName.getValue()) {

                    entitiesGroups = entitiesGroups.stream()
                            .filter(group -> group.getName().equals(namePath))
                            .findAny()
                            .map(EntitiesGroup::getInnerGroups)
                            .orElse(null);

                    if (entitiesGroups == null) {

                        Collection<String> entities = groupNames.get(fullGroupName);
                        String message = "Group tree doesn't contains a node [{}] used in {}";
                        errors.add(new ValidationResult(message, NODE_GROUP_TREE_ABSENT, namePath, entities));
                        break;
                    }
                }
            }
        }

        return errors;
    }

    /**
     * Gets the duplicate names.
     *
     * @param ctx the ctx
     * @return the duplicate names
     */
    private Collection<ValidationResult> validateNames(@Nonnull DataModel model) {

        Collection<String> allNames = ModelUtils.findAllTopLevelNames(model).stream()
                .map(String::toLowerCase)
                .collect(Collectors.toList());

        Set<String> uniques = new HashSet<>();
        Collection<String> duplicateNames = allNames.stream()
                .filter(e -> !uniques.add(e))
                .collect(Collectors.toList());

        if (!duplicateNames.isEmpty()) {
            return Collections.singleton(new ValidationResult(
                    "Model contains duplicate top level names [Letter case not considered].",
                    DUPLICATE_ENTITIY_NAMES, duplicateNames));
        }

        return Collections.emptyList();
    }

    /**
     * Check consistency of model over attribute groups(display groups).
     *
     * @param ctx - context which contains all necessary info about model.
     * @return collection of validation results
     */
    private Collection<ValidationResult> validateDisplayGroups(@Nonnull DataModel model) {

        Collection<ValidationResult> validationResults = new HashSet<>();

        for (Entity entity : model.getEntities()) {

            Collection<String> requiredAttr = entity.getAttributeGroups()
                    .stream()
                    .map(AttributeGroup::getAttributes)
                    .flatMap(Collection::stream)
                    .collect(Collectors.toSet());

            Collection<String> absentAttributes = getAbsent(entity, requiredAttr, model.getNestedEntities());

            absentAttributes.stream()
                    .map(name -> new ValidationResult("Display group of entity [{}] contain absent attribute(s) [{}]",
                            UNKNOWN_ATTR_IN_DISPLAY_GROUP, entity.getDisplayName(), name))
                    .collect(Collectors.toCollection(() -> validationResults));
        }

        for (LookupEntity entity : model.getLookupEntities()) {

            Collection<String> requiredAttrs = entity.getAttributeGroups()
                    .stream()
                    .map(AttributeGroup::getAttributes)
                    .flatMap(Collection::stream)
                    .collect(Collectors.toSet());

            Collection<String> notPresentedAttrs = getAbsent(entity, requiredAttrs, emptyList());
            notPresentedAttrs.stream()
                    .map(name -> new ValidationResult("Unknown attribute(s) [{}] in display group of entity [{}]",
                            UNKNOWN_ATTR_IN_DISPLAY_GROUP, name, entity.getDisplayName()))
                    .collect(Collectors.toCollection(() -> validationResults));
        }

        return validationResults;
    }

    /**
     * Find duplicate attribute names in lookups, entities, nested entities.
     *
     * @param ctx context to check.
     * @return list with errors.
     */
    private Collection<ValidationResult> validateAttributes(@Nonnull final DataModel model) {

        Collection<ValidationResult> errors = new ArrayList<>();

        model.getLookupEntities().forEach(el -> validateAttributes(errors, el, el.getName(), MetaType.LOOKUP));
        model.getEntities().forEach(el -> validateAttributes(errors, el, el.getName(), MetaType.ENTITY));
        model.getNestedEntities().forEach(el -> validateAttributes(errors, el, el.getName(), MetaType.NESTED_ENTITY));

        return errors;
    }

    /**
     * Check consistency of model over merge settings.
     *
     * @param ctx - context which contains all necessary info about model.
     * @return collection of validation results
     */
    private Collection<ValidationResult> validateMergeSettings(@Nonnull DataModel model) {

        Collection<ValidationResult> validationResults = new ArrayList<>();
        for (Entity entity : model.getEntities()) {

            if (entity.getMergeSettings() == null) {
                continue;
            }

            //check attrs
            List<MergeAttribute> attributes = entity.getMergeSettings().getBvtSettings() == null ?
                    Collections.emptyList() :
                    entity.getMergeSettings().getBvtSettings().getAttributes();

            Collection<String> requiredAttrs = attributes.stream()
                    .map(MergeAttribute::getName)
                    .collect(Collectors.toSet());

            requiredAttrs.stream()
                    .filter(attr -> StringUtils.isBlank(attr) || ModelUtils.isCompoundPath(attr))
                    .map(name -> new ValidationResult("Merge attr [{}] is incorrect in entity [{}]", MERGE_ATTR_INCORRECT, name,
                            entity.getDisplayName()))
                    .collect(Collectors.toCollection(() -> validationResults));

            Collection<String> absentAttrs = getAbsent(entity, requiredAttrs, model.getNestedEntities());
            absentAttrs.stream()
                    .map(name -> new ValidationResult("Entity [{}] contain merge attr [{}] which is absent", MERGE_ATTR_ABSENT,
                            entity.getDisplayName(), name))
                    .collect(Collectors.toCollection(() -> validationResults));

            //check merge source system
            Collection<SourceSystem> sourceSystems = attributes.stream()
                    .map(MergeAttribute::getSourceSystemsConfigs)
                    .flatMap(Collection::stream)
                    .collect(toList());

            if (entity.getMergeSettings().getBvrSettings() != null) {
                sourceSystems.addAll(entity.getMergeSettings().getBvrSettings().getSourceSystemsConfigs());
            }

            sourceSystems.stream()
                    .filter(Objects::nonNull)
                    .filter(sourceSystemDef -> !isValidSourceSystem(sourceSystemDef))
                    .map(SourceSystem::getName)
                    .distinct()
                    .map(name -> new ValidationResult("Entity [{}] contain Merge source system [{}] with incorrect params",
                            MERGE_SOURCE_SYSTEM_INCORRECT, entity.getDisplayName(), name))
                    .collect(Collectors.toCollection(() -> validationResults));

            sourceSystems.stream()
                    .map(SourceSystem::getName)
                    .distinct()
                    .filter(source -> !isSourceSystemPresent(source))
                    .map(name -> new ValidationResult("Entity [{}] contain Merge source system [{}] which is absent in system",
                            MERGE_SOURCE_SYSTEM_ABSENT, entity.getDisplayName(), name))
                    .collect(Collectors.toCollection(() -> validationResults));
        }

        return validationResults;
    }

    /**
     * Check references to enumerations.
     *
     * @param ctx the ctx
     * @return the collection of validation errors
     */
    private Collection<ValidationResult> validateReferencesToEnumerations(@Nonnull final DataModel model) {

        Collection<ValidationResult> errors = new ArrayList<>();
        Collection<SimpleMetaModelAttribute> allEnumLinkAttr
            = getAttributes(model,
                    attr -> (attr.getSimpleDataType() == null && !StringUtils.isBlank(attr.getEnumDataType())));

        for (SimpleMetaModelAttribute enumLink : allEnumLinkAttr) {

            boolean exists = metaModelService
                    .instance(Descriptors.ENUMERATIONS)
                    .exists(enumLink.getEnumDataType());

            if (!exists) {
                errors.add(new ValidationResult("Attr [{}] has link to enum [{}] which is absent", ENUMERATION_ABSENT, enumLink.getDisplayName(),
                        enumLink.getEnumDataType()));
            }

            errors.addAll(validateCustomProperties(enumLink.getName(), enumLink.getCustomProperties()));
        }

        return errors;
    }

    /**
     * Check references to lookup entities.
     *
     * @param ctx the ctx
     * @return the collection of validation errors
     */
    private Collection<ValidationResult> validateReferencesToLookups(@Nonnull final DataModel model) {

        Collection<ValidationResult> errors = new ArrayList<>();
        Collection<SimpleMetaModelAttribute> allLookupLinkAttr =
                getAttributes(model,
                        attr -> (attr.getSimpleDataType() == null && !isBlank(attr.getLookupEntityType())));

        for (SimpleMetaModelAttribute link : allLookupLinkAttr) {

            LookupEntity lookupEntityById = model.getLookupEntities()
                    .stream()
                    .filter(entity -> entity.getName().equals(link.getLookupEntityType()))
                    .findAny()
                    .orElse(null);

            if (lookupEntityById == null) {
                errors.add(new ValidationResult("Attr [{}] has link to lookup entity [{}] which is absent",
                        LOOKUP_ENTITY_ABSENT, link.getDisplayName(), link.getLookupEntityType()));
            }

            errors.addAll(validateCustomProperties(link.getName(), link.getCustomProperties()));
        }

        return errors;
    }

    /**
     * Check references to nested entities.
     *
     * @param ctx the ctx
     * @return the collection of validation errors
     */
    private Collection<ValidationResult> validateReferencesToNesteds(@Nonnull final DataModel model) {

        Collection<ValidationResult> errors = new ArrayList<>();
        Collection<ComplexMetaModelAttribute> entityComplexAttrs = model.getEntities().stream()
                .map(ComplexAttributesHolderEntity::getComplexAttribute)
                .flatMap(Collection::stream)
                .collect(Collectors.toList());

        Collection<ComplexMetaModelAttribute> nestedComplexAttrs = model.getNestedEntities().stream()
                .map(ComplexAttributesHolderEntity::getComplexAttribute)
                .flatMap(Collection::stream)
                .collect(Collectors.toList());

        Stream.concat(entityComplexAttrs.stream(), nestedComplexAttrs.stream())
                .forEach(ca -> {
                    if (StringUtils.isBlank(ca.getNestedEntityName())) {
                        errors.add(new ValidationResult("Attr [{}] has blank nested entity name field.", NESTED_ENTITY_NAME_ABSENT, ca.getName()));
                    } else if (model.getNestedEntities().stream().noneMatch(ne -> StringUtils.equals(ne.getName(), ca.getNestedEntityName()))) {
                        errors.add(new ValidationResult("Attr [{}] defines link to nested entity [{}] which is absent",
                                NESTED_ENTITY_ABSENT, ca.getName(), ca.getNestedEntityName()));
                    }
                });

        return errors;
    }

    /**
     * Check references to group from entities and lookups.
     *
     * @param ctx context.
     * @return validation errors.
     */
    private Collection<ValidationResult> validateReferencesToGroups(@Nonnull final DataModel model) {

        EntitiesGroup rootGroup = model.getEntitiesGroup();

        Set<String> groupNames = new HashSet<>();
        getGroupNames("", rootGroup, groupNames);
        Collection<ValidationResult> errors = new ArrayList<>();

        List<Entity> entities = model.getEntities();
        for (Entity entity : entities) {
            if (!groupNames.contains(entity.getGroupName())) {
                String message = "Group tree doesn't contains a node [{}] used in [{}]";
                errors.add(new ValidationResult(message, NODE_GROUP_TREE_ABSENT, entity.getGroupName(), entity.getName()));
            }
        }

        List<LookupEntity> lookups = model.getLookupEntities();
        for (LookupEntity lookup : lookups) {
            if (!groupNames.contains(lookup.getGroupName())) {
                String message = "Group tree doesn't contains a node [{}] used in [{}]";
                errors.add(new ValidationResult(message, NODE_GROUP_TREE_ABSENT, lookup.getGroupName(), lookup.getName()));
            }
        }

        return errors;
    }

    /**
     * Check relations for CP and basic stuff conformance.
     * @param model the model to check
     * @return validation results.
     */
    private Collection<ValidationResult> validateRelations(final @Nonnull DataModel model) {

        return model.getRelations().stream()
                .flatMap(r -> {

                    final List<ValidationResult> errors = new ArrayList<>();
                    if (StringUtils.isNotBlank(r.getFromEntity())
                     && model.getEntities().stream().noneMatch(en -> StringUtils.equals(en.getName(), r.getFromEntity()))) {
                        String message = "Relation [{}] has FROM reference [{}] which is absent.";
                        errors.add(new ValidationResult(message, RELATION_FROM_ENTITY_ABSENT, r.getName(), r.getFromEntity()));
                    }

                    if (StringUtils.isNotBlank(r.getToEntity())
                     && model.getEntities().stream().noneMatch(en -> StringUtils.equals(en.getName(), r.getToEntity()))) {
                        String message = "Relation [{}] has TO reference [{}] which is absent.";
                        errors.add(new ValidationResult(message, RELATION_TO_ENTITY_ABSENT, r.getName(), r.getToEntity()));
                    }

                    errors.addAll(validateCustomProperties(r.getName(), r.getCustomProperties()));
                    errors.addAll(r.getSimpleAttribute().stream()
                            .flatMap(attr -> validateCustomProperties(attr.getName(), attr.getCustomProperties()).stream())
                            .collect(toList()));
                    errors.addAll(r.getComplexAttribute().stream()
                            .flatMap(attr -> validateCustomProperties(attr.getName(), attr.getCustomProperties()).stream())
                            .collect(toList()));
                    errors.addAll(r.getArrayAttribute().stream()
                            .flatMap(attr -> validateCustomProperties(attr.getName(), attr.getCustomProperties()).stream())
                            .collect(toList()));

                    return errors.stream();
                })
                .collect(toList());
    }

    /**
     * Check relation timelines.
     *
     * @param ctx the ctx
     * @return the multimap<? extends validation error type,? extends string>
     */
    private Collection<ValidationResult> validateTimelines(final @Nonnull DataModel model) {

        Collection<ValidationResult> errors = new ArrayList<>();
        Map<String, PeriodBoundary> periods = getPeriods(model);

        model.getRelations().stream()
                .map(rel -> validatePeriodBoundaries(periods, rel))
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(() -> errors));

        model.getEntities().forEach(entity -> getLinkToLookup(entity).stream()
                .map(lookupName -> new Relation().withFromEntity(entity.getName()).withToEntity(lookupName))
                .map(rel -> validatePeriodBoundaries(periods, rel))
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(() -> errors)));

        model.getLookupEntities().forEach(entity -> getLinkToLookup(entity).stream()
                .map(lookupName -> new Relation().withFromEntity(entity.getName()).withToEntity(lookupName))
                .map(rel -> validatePeriodBoundaries(periods, rel))
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(() -> errors)));

        model.getNestedEntities().forEach(entity -> getLinkToLookup(entity).stream()
                .map(lookupName -> Pair.of(lookupName, getEntitiesByNestedEntity(model, entity.getName())))
                .filter(p -> CollectionUtils.isNotEmpty(p.getValue()))
                .map(p -> p.getValue().stream().map(v -> new Relation().withFromEntity(v.getName()).withToEntity(p.getKey())).collect(Collectors.toList()))
                .flatMap(Collection::stream)
                .filter(rel -> rel.getFromEntity() != null)
                .map(rel -> validatePeriodBoundaries(periods, rel))
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(() -> errors)));

        return errors;
    }

    /**
     * Check overlaping between two ends of relation.
     *
     * @param periods all periods.
     * @param r relation def.
     * @return valudation result
     */
    private ValidationResult validatePeriodBoundaries(Map<String, PeriodBoundary> periods, Relation r) {

        String fromEntity = r.getFromEntity();
        String toEntity = r.getToEntity();

        Interval from = getInterval(periods.get(fromEntity));
        Interval to = getInterval(periods.get(toEntity));

        if (!from.overlaps(to)) {
            return new ValidationResult("Timeline is not overlaping.From entity [{}], To entity [{}]", TIMELINE_NOT_OVERLAPPING, fromEntity, toEntity);
        } else {
            return null;
        }
    }

    /**
     * Check references to MU.
     */
    private Collection<ValidationResult> validateReferencesToMeasurementUnits(@Nonnull final DataModel model) {

        // && measureValue.getId().equals(attr.getMeasureSettings().getCategoryId()
        MeasurementUnitsInstance mui = metaModelService.instance(Descriptors.MEASUREMENT_UNITS);

        Collection<SimpleMetaModelAttribute> linkedElements
            = getAttributes(model, attr -> attr.getMeasureSettings() != null);

        List<ValidationResult> validationResult = new ArrayList<>();
        for (SimpleMetaModelAttribute sa : linkedElements) {

            AttributeMeasurementSettings ams = sa.getMeasureSettings();
            if (!mui.exists(ams.getCategoryId())) {
                validationResult.add(new ValidationResult(
                        "[{}] attribute's measurement settings [{}, {}] refer to an undefined category.",
                        MEASUREMENT_CATEGORY_INCORRECT, sa.getName(),
                        sa.getMeasureSettings().getDefaultUnitId(), sa.getMeasureSettings().getCategoryId()));
            } else if (!mui.getCategory(ams.getCategoryId()).exists(ams.getDefaultUnitId())) {
                validationResult.add(new ValidationResult(
                        "[{}] attribute's measurement settings [{}, {}] refer to an undefined unit.",
                        MEASUREMENT_UNIT_INCORRECT, sa.getName(),
                        sa.getMeasureSettings().getDefaultUnitId(), sa.getMeasureSettings().getCategoryId()));
            }
        }

        return validationResult;
    }

    /**
     * Check modification of relation type on elements with data.
     *
     * @param ctx context.
     * @return list with errors(if any).
     */
    private Collection<? extends ValidationResult> validateRelationTypeChange(final @Nonnull DataModel model) {
        final List<Relation> modifiedRelations = model.getRelations().stream()
                .filter(r -> {
                            RelationElement existing = metaModelService.instance(Descriptors.DATA).getRelation(r.getName());
                            return existing != null
                            && ((r.getRelType() == RelType.CONTAINS && !existing.isContainment())
                             || (r.getRelType() == RelType.MANY_TO_MANY && !existing.isManyToMany())
                             || (r.getRelType() == RelType.REFERENCES && !existing.isReference()));
                         })
                .collect(toList());
        final String message = "Relation type of [{}] can't be modified, because element may have data";
        return modifiedRelations.isEmpty() ?
                Collections.emptyList() :
                modifiedRelations.stream()
                        .map(relationDef -> new ValidationResult(message, MODIFIED_RELATION_TYPE_ON_ELEMENT_WITH_DATA, relationDef.getName()))
                        .collect(toList());
    }

    /**
     * Search for duplicate attributes.
     *
     * @param errors list with errors.
     * @param el element to check.
     * @param elName element name.
     * @param type
     */
    private void validateAttributes(
            Collection<ValidationResult> errors,
            AbstractEntity<?> el,
            String elName,
            MetaType type
    ) {
        Set<String> dupl = new HashSet<>();
        Set<String> names = new HashSet<>();
        boolean isSearchable = false;
        for (SimpleMetaModelAttribute sa : el.getSimpleAttribute()) {

            if (sa.isSearchable()) {
                isSearchable = true;
            }

            if (names.contains(sa.getName())) {
                dupl.add(sa.getName());
            }

            if (sa.getSimpleDataType() == null && !StringUtils.isEmpty(sa.getLinkDataType())
            && (sa.isDisplayable() || sa.isSearchable() || sa.isMainDisplayable() || !sa.isNullable() || sa.isUnique())) {
                String message = "Link attribute shouldn't be displayable, searchable, main displayable, required or unique. Entity name [{}]. Attribute name [{}].";
                errors.add(new ValidationResult(message, LINK_ATTRIBUTE_INCORRECT, elName, sa.getName()));
            }

            names.add(sa.getName());

            validateAttributeGenerationStrategy(sa, el, errors);
        }

        for (ArrayMetaModelAttribute aa : el.getArrayAttribute()) {
            if (names.contains(aa.getName())) {
                dupl.add(aa.getName());
            }
            if (aa.isSearchable()) {
                isSearchable = true;
            }
            names.add(aa.getName());

            validateAttributeGenerationStrategy(aa, el, errors);
        }

        if (el instanceof LookupEntity) {
            LookupEntity lookup = (LookupEntity) el;
            if (lookup.getCodeAttribute().isSearchable()) {
                isSearchable = true;
            } else {
                CodeMetaModelAttribute cad = lookup.getAliasCodeAttributes().stream()
                        .filter(CodeMetaModelAttribute::isSearchable)
                        .findFirst()
                        .orElse(null);

                if (Objects.nonNull(cad)) {
                    isSearchable = true;
                }
            }

            Stream.concat(Stream.of(lookup.getCodeAttribute()), lookup.getAliasCodeAttributes().stream())
                .forEach(codeAttr -> validateAttributeGenerationStrategy(codeAttr, lookup, errors));

            errors.addAll(validateCustomProperties(
                    lookup.getCodeAttribute().getName(),
                    lookup.getCodeAttribute().getCustomProperties())
            );
        }

        if (!isSearchable && type != MetaType.NESTED_ENTITY) {
            String message = "Entity or Lookup [{}] does not contain at least one searchable attribute on the first level.";
            errors.add(new ValidationResult(message, NO_SEARCHABLE_ATTRIBUTE, elName));
        }

        if (!dupl.isEmpty()) {
            for (String duplName : dupl) {
                String message = "Found more than one attribute with name [{}]  in [{}]";
                errors.add(new ValidationResult(message, DUPL_ATTRIBUTE, duplName, elName));
            }
        }

        el.getSimpleAttribute().forEach(
                attr -> errors.addAll(validateCustomProperties(attr.getName(), attr.getCustomProperties()))
        );
        el.getArrayAttribute().forEach(
                attr -> errors.addAll(validateCustomProperties(attr.getName(), attr.getCustomProperties()))
        );
        if (el instanceof ComplexAttributesHolderEntity) {
            final ComplexAttributesHolderEntity<?> complexAttributesHolder = (ComplexAttributesHolderEntity<?>) el;
            complexAttributesHolder.getComplexAttribute()
                .forEach(attr -> {
                    errors.addAll(validateCustomProperties(attr.getName(), attr.getCustomProperties()));
                    validateAttributeGenerationStrategy(attr, el, errors);
                });
        }

        validateEntityGenerationStrategy(el, errors);
    }

    private void validateEntityGenerationStrategy(AbstractEntity<?> def, Collection<ValidationResult> errors) {

        ValueGenerationStrategy eigsd = null;
        if ((def instanceof LookupEntity)) {
            eigsd = ((LookupEntity) def).getExternalIdGenerationStrategy();
        } else if ((def instanceof Entity)) {
            eigsd = ((Entity) def).getExternalIdGenerationStrategy();
        }

        if (Objects.isNull(eigsd)) {
            return;
        }

        ValueGenerationStrategyType strategyType = eigsd.getStrategyType();
        if (strategyType == ValueGenerationStrategyType.CUSTOM) {

            CustomValueGenerationStrategy custom = (CustomValueGenerationStrategy) eigsd;
            String classFQN = custom.getClassName();
            if (StringUtils.isBlank(classFQN)) {
                String message = "Entity defines CUSTOM generation strategy, but the class name is empty. Entity name [{}].";
                errors.add(new ValidationResult(message, ENTITY_GENERATION_STRATEGY_INCORRECT, strategyType.name(), def.getName()));
                return;
            }

            try {

                Class<?> implementor = Thread
                        .currentThread()
                        .getContextClassLoader()
                        .loadClass(classFQN);

                if (!ExternalIdValueGenerator.class.isAssignableFrom(implementor)) {
                    String message = "Entity defines {} generation strategy, for which the implementing class does not implement AttributeValueGenerator. Entity name [{}].";
                    errors.add(new ValidationResult(message, ENTITY_GENERATION_STRATEGY_INCORRECT, strategyType.name(), def.getName()));
                    return;
                }


                Constructor<?>[] cs = implementor.getDeclaredConstructors();
                boolean hasEmptyCtor = Stream.of(cs).anyMatch(c -> c.getParameterCount() == 0);
                if (!hasEmptyCtor) {
                    String message = "Entity defines {} generation strategy, for which the implementing class does not define an empty constructor. Entity name [{}].";
                    errors.add(new ValidationResult(message, ENTITY_GENERATION_STRATEGY_INCORRECT, strategyType.name(), def.getName()));
                }

            } catch (ClassNotFoundException e) {
                String message = "Entity defines {} generation strategy, for which the implementing class was not found. Entity name [{}].";
                errors.add(new ValidationResult(message, ENTITY_GENERATION_STRATEGY_INCORRECT, strategyType.name(), def.getName()));
            }
        }
    }

    private void validateAttributeGenerationStrategy(MetaModelAttribute attr, AbstractEntity<?> def, Collection<ValidationResult> errors) {

        ValueGenerationStrategy eigsd = attr.getValueGenerationStrategy();
        if (Objects.isNull(eigsd)) {
            return;
        }

        ValueGenerationStrategyType strategyType = eigsd.getStrategyType();
        if ((strategyType == ValueGenerationStrategyType.CONCAT || strategyType == ValueGenerationStrategyType.RANDOM)
          && (!(attr instanceof SimpleTypeMetaModelAttribute) || ((SimpleTypeMetaModelAttribute) attr).getSimpleDataType() != SimpleDataType.STRING)) {
            String message = "Attribute defines {} generation strategy, which is incompatible with attribute value type. Entity name [{}]. Attribute name [{}].";
            errors.add(new ValidationResult(message, ATTRIBUTE_GENERATION_STRATEGY_INCORRECT, strategyType.name(), def.getName(), attr.getName()));
        } else if (strategyType == ValueGenerationStrategyType.CUSTOM) {

            CustomValueGenerationStrategy custom = (CustomValueGenerationStrategy) eigsd;
            String classFQN = custom.getClassName();
            if (StringUtils.isBlank(classFQN)) {
                String message = "Attribute defines CUSTOM generation strategy, but the class name is empty. Entity name [{}]. Attribute name [{}].";
                errors.add(new ValidationResult(message, ATTRIBUTE_GENERATION_STRATEGY_INCORRECT, strategyType.name(), def.getName(), attr.getName()));
                return;
            }

            try {

                Class<?> implementor = Thread
                        .currentThread()
                        .getContextClassLoader()
                        .loadClass(classFQN);

                if (!AttributeValueGenerator.class.isAssignableFrom(implementor)) {
                    String message = "Attribute defines {} generation strategy, for which the implementing class does not implement AttributeValueGenerator. Entity name [{}]. Attribute name [{}].";
                    errors.add(new ValidationResult(message, ATTRIBUTE_GENERATION_STRATEGY_INCORRECT, strategyType.name(), def.getName(), attr.getName()));
                    return;
                }

                AttributeValueGenerator i = (AttributeValueGenerator) implementor
                    .getDeclaredConstructor()
                    .newInstance();

                Class<?> retval = i.getOutputType();
                if (attr instanceof SimpleTypeMetaModelAttribute) {

                    SimpleTypeMetaModelAttribute asad = (SimpleTypeMetaModelAttribute) attr;
                    if ((asad.getSimpleDataType() == SimpleDataType.STRING && retval != String.class)
                     || (asad.getSimpleDataType() == SimpleDataType.INTEGER && retval != Long.class)
                     || (asad.getSimpleDataType() == SimpleDataType.BOOLEAN && retval != Boolean.class)
                     || (asad.getSimpleDataType() == SimpleDataType.BLOB && retval != BinaryLargeValue.class)
                     || (asad.getSimpleDataType() == SimpleDataType.CLOB && retval != CharacterLargeValue.class)
                     || (asad.getSimpleDataType() == SimpleDataType.DATE && retval != LocalDate.class)
                     || (asad.getSimpleDataType() == SimpleDataType.MEASURED && retval != MeasuredValue.class)
                     || (asad.getSimpleDataType() == SimpleDataType.NUMBER && retval != Double.class)
                     || (asad.getSimpleDataType() == SimpleDataType.TIME && retval != LocalTime.class)
                     || (asad.getSimpleDataType() == SimpleDataType.TIMESTAMP && retval != LocalDateTime.class)) {
                        String message = "Attribute defines {} generation strategy, for which the implementing class value type is incompatible with attribute value type. "
                                + "Entity name [{}]. Attribute name [{}].";
                        errors.add(new ValidationResult(message, ATTRIBUTE_GENERATION_STRATEGY_INCORRECT, strategyType.name(), def.getName(), attr.getName()));
                    }
                } else if ((attr instanceof ArrayMetaModelAttribute || attr instanceof ComplexMetaModelAttribute) && retval != List.class) {
                    String message = "Attribute defines {} generation strategy, for which the implementing class value type is incompatible with attribute value type. "
                            + "Entity name [{}]. Attribute name [{}].";
                    errors.add(new ValidationResult(message, ATTRIBUTE_GENERATION_STRATEGY_INCORRECT, strategyType.name(), def.getName(), attr.getName()));
                }

            } catch (ClassNotFoundException e) {
                String message = "Attribute defines {} generation strategy, for which the implementing class was not found. Entity name [{}]. Attribute name [{}].";
                errors.add(new ValidationResult(message, ATTRIBUTE_GENERATION_STRATEGY_INCORRECT, strategyType.name(), def.getName(), attr.getName()));
            } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) {
                String message = "Attribute defines {} generation strategy, for which the implementing class does not define an empty constructor. Entity name [{}]. Attribute name [{}].";
                errors.add(new ValidationResult(message, ATTRIBUTE_GENERATION_STRATEGY_INCORRECT, strategyType.name(), def.getName(), attr.getName()));
            }
        }
    }

    /**
     * @param sourceSystemDef - source system
     * @return true if source system is valid
     */
    private boolean isValidSourceSystem(SourceSystem sourceSystemDef) {

        if (isBlank(sourceSystemDef.getName())) {
            return false;
        }

        Integer weight = sourceSystemDef.getWeight();
        return weight != null && weight.intValue() >= 0 && weight.intValue() <= 100;
    }

    private boolean isSourceSystemPresent(String sourceSystem) {

        if (StringUtils.isBlank(sourceSystem)) {
            return false;
        }

        return metaModelService
                .instance(Descriptors.SOURCE_SYSTEMS)
                .exists(sourceSystem);
    }

    private Interval getInterval(PeriodBoundary period) {
        Interval result;
        if (period != null) {
            Date from = Optional.ofNullable(period.getStart()).map(ConvertUtils::localDateTime2Date).orElse(MIN_DATE_VALUE);
            Date to = Optional.ofNullable(period.getEnd()).map(ConvertUtils::localDateTime2Date).orElse(MAX_DATE_VALUE);
            result = new Interval(from.getTime(), to.getTime());
        } else {
            result = NULL_INTERVAL_VALUE;
        }
        return result;
    }

    private Collection<Entity> getEntitiesByNestedEntity(DataModel model, String nestedName) {
        return model.getEntities()
                .stream()
                .filter(e -> e.getComplexAttribute().stream().anyMatch(ca -> ca.getName().equals(nestedName)))
                .distinct()
                .collect(Collectors.toList());
    }

    /**
     * Collect periods for all entities\lookups.
     *
     * @param ctx update model request context.
     * @return Map with collected periods.
     */
    private Map<String, PeriodBoundary> getPeriods(DataModel model) {

        // Collect period for all entities/lookups
        final Map<String, PeriodBoundary> periods = new HashMap<>();
        model.getEntities().stream()
                .map(entity -> Pair.of(entity.getName(), getPeriod(entity.getValidityPeriod())))
                .collect(Collectors.toMap(Pair::getKey, Pair::getValue, (e1, e2) -> e1, () -> periods));

        model.getLookupEntities().stream()
                .map(entity -> Pair.of(entity.getName(), getPeriod(entity.getValidityPeriod())))
                .collect(Collectors.toMap(Pair::getKey, Pair::getValue, (e1, e2) -> e1, () -> periods));

        return periods;
    }

    private PeriodBoundary getPeriod(PeriodBoundary period) {
        if (isNull(period)) {
            period = new PeriodBoundary();
            period.setStart(ConvertUtils.date2LocalDateTime(ValidityPeriodUtils.getGlobalValidityPeriodStart()));
            period.setEnd(ConvertUtils.date2LocalDateTime(ValidityPeriodUtils.getGlobalValidityPeriodEnd()));
        } else {
            if (isNull(period.getStart())) {
                period.setStart(ConvertUtils.date2LocalDateTime(ValidityPeriodUtils.getGlobalValidityPeriodStart()));
            }
            if (isNull(period.getEnd())) {
                period.setEnd(ConvertUtils.date2LocalDateTime(ValidityPeriodUtils.getGlobalValidityPeriodEnd()));
            }
        }
        return period;
    }

    private Collection<String> getLinkToLookup(AbstractEntity<?> attrHolder) {

        Collection<String> links = new ArrayList<>();
        attrHolder.getArrayAttribute()
                .stream()
                .filter(sa -> !isEmpty(sa.getLookupEntityType()))
                .map(ArrayMetaModelAttribute::getLookupEntityType)
                .collect(Collectors.toCollection(() -> links));
        attrHolder.getSimpleAttribute()
                .stream()
                .filter(sa -> !isEmpty(sa.getLookupEntityType()))
                .map(SimpleMetaModelAttribute::getLookupEntityType)
                .collect(Collectors.toCollection(() -> links));

        return links;
    }

    /**
     * Gets all attribute names, not present in the model.
     *
     * @param attrHolder the attr holder
     * @param attrNames the attr names
     * @param nestedEntities the nested entities
     * @return the all not presented attr
     */
    private Collection<String> getAbsent(AbstractEntity<?> attrHolder, Collection<String> attrNames, Collection<NestedEntity> nestedEntities) {
        return attrNames.stream()
                .filter(attrName -> findModelAttribute(attrName, attrHolder, nestedEntities) == null)
                .collect(toList());
    }

    /**
     * Create set with group names.
     *
     * @param path group path.
     * @param group entity group.
     * @param groupNames group names.
     */
    private void getGroupNames(String path, EntitiesGroup group, Set<String> groupNames) {
        path = StringUtils.isEmpty(path) ? group.getName() : String.join(".", path, group.getName());
        groupNames.add(path);
        if (CollectionUtils.isNotEmpty(group.getInnerGroups())) {
            for (EntitiesGroup innerGroup : group.getInnerGroups()) {
                getGroupNames(path, innerGroup, groupNames);
            }
        }
    }

    /**
     * @param predicate - filtering condition
     * @return collection of @link{com.unidata.mdm.meta.SimpleAttributeDef}.
     */
    private Collection<SimpleMetaModelAttribute> getAttributes(DataModel model, Predicate<? super SimpleMetaModelAttribute> predicate) {

        Collection<SimpleMetaModelAttribute> la = model.getLookupEntities().stream()
                .map(LookupEntity::getSimpleAttribute)
                .flatMap(Collection::stream)
                .filter(predicate)
                .collect(toList());
        Collection<SimpleMetaModelAttribute> ra = model.getEntities().stream()
                .map(Entity::getSimpleAttribute)
                .flatMap(Collection::stream)
                .filter(predicate)
                .collect(Collectors.toList());
        Collection<SimpleMetaModelAttribute> na = model.getNestedEntities().stream()
                .map(NestedEntity::getSimpleAttribute)
                .flatMap(Collection::stream)
                .filter(predicate)
                .collect(Collectors.toList());

        Collection<SimpleMetaModelAttribute> allAttr = new ArrayList<>(ra.size() + la.size() + na.size());
        allAttr.addAll(la);
        allAttr.addAll(ra);
        allAttr.addAll(na);
        return allAttr;
    }
}
